package bjut.core.Message;

import bjut.core.extension.ExtensionLoader;
import bjut.core.constant.RpcConstant;
import bjut.core.enums.CompressTypeEnum;
import bjut.core.enums.SerializationTypeEnum;
import bjut.core.interfaces.Compress;
import bjut.core.interfaces.Serializer;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;

/**
 * custom protocol decoder
 * <pre>
 *   0     1     2     3     4        5     6     7     8         9          10      11     12  13  14   15 16
 *   +-----+-----+-----+-----+--------+----+----+----+------+-----------+-------+----- --+-----+-----+-------+
 *   |   magic   code        |version | full length         | messageType| codec|compress|    RequestId       |
 *   +-----------------------+--------+---------------------+-----------+-----------+-----------+------------+
 *   |                                                                                                       |
 *   |                                         body                                                          |
 *   |                                                                                                       |
 *   |                                        ... ...                                                        |
 *   +-------------------------------------------------------------------------------------------------------+
 * 4B  magic code（魔法数）   1B version（版本）   4B full length（消息长度）    1B messageType（消息类型）
 * 1B compress（压缩类型） 1B codec（序列化类型）    4B  requestId（请求的Id）
 * body（object类型数据）
 * </pre>
 * <p>
 * {@link LengthFieldBasedFrameDecoder} is a length-based decoder , used to solve TCP unpacking and sticking problems.
 * </p>
 *
 * @Author: zhuzhiming
 * @Date: 2022/4/8 21:45
 */
@Slf4j
public class RpcMessageDecoder extends LengthFieldBasedFrameDecoder {


    // lengthFieldOffset: magic code is 4B, and version is 1B, and then full length. so value is 5
    // lengthFieldLength: full length is 4B. so value is 4
    // lengthAdjustment: full length include all data and read 9 bytes before, so the left length is (fullLength-9). so values is -9
    // initialBytesToStrip: we will check magic code and version manually, so do not strip any bytes. so values is 0
    public RpcMessageDecoder() {
        this(RpcConstant.MAX_FRAME_LENGTH, 5, 4, -9, 0);
    }


    /**
     * @param maxFrameLength      Maximum frame length. It decide the maximum length of data that can be received.
     *                            If it exceeds, the data will be discarded.
     * @param lengthFieldOffset   Length field offset. The length field is the one that skips the specified length of byte.
     * @param lengthFieldLength   The number of bytes in the length field.
     * @param lengthAdjustment    The compensation value to add to the value of the length field
     * @param initialBytesToStrip Number of bytes skipped.
     *                            If you need to receive all of the header+body data, this value is 0
     *                            if you only want to receive the body data, then you need to skip the number of bytes consumed by the header.
     */
    public RpcMessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) {
        super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip);
    }

    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        Object decode = super.decode(ctx, in);

        if (decode instanceof ByteBuf) {

            ByteBuf frame = (ByteBuf) decode;

            if (frame.readableBytes() > RpcConstant.HEAD_LENGTH) {
                try {
                    return decodeFrame(frame);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    frame.release();
                }
            }
        }
        return decode;
    }

    private Object decodeFrame(ByteBuf frame) {

        checkMagicNumber(frame);
        checkVersion(frame);

        int fullLength = frame.readInt();
        byte messageType = frame.readByte();
        byte codeType = frame.readByte();
        byte compressType = frame.readByte();
        int requestId = frame.readInt();

        RpcMessage rpcMessage = RpcMessage.builder()
                .messageType(messageType)
                .codec(codeType)
                .compress(compressType)
                .requestId(requestId)
                .build();

        if (messageType == RpcConstant.HEART_REQUEST_TYPE) {
            rpcMessage.setData(RpcConstant.HEART_REQUEST_DATA);
            return rpcMessage;
        }

        if (messageType == RpcConstant.HEART_RESPONSE_TYPE) {
            rpcMessage.setData(RpcConstant.HEART_RESPONSE_DATA);
            return rpcMessage;
        }

        int bodyLength = fullLength - RpcConstant.HEAD_LENGTH;

        if (bodyLength > 0) {

            byte[] bytes = new byte[bodyLength];
            frame.readBytes(bytes);

            String compressName = CompressTypeEnum.getCompressType(compressType);
            Compress compress = ExtensionLoader.getExtensionLoader(Compress.class).getExtension(compressName);
            byte[] decompress = compress.decompress(bytes);

            String serialType = SerializationTypeEnum.getSerialType(codeType);
            Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension(serialType);

            if (messageType == RpcConstant.REQUEST_TYPE) {
                RpcRequest rpcRequest = serializer.deserialize(decompress, RpcRequest.class);
                rpcMessage.setData(rpcRequest);
            } else {
                RpcResponse rpcResponse = serializer.deserialize(decompress, RpcResponse.class);
                rpcMessage.setData(rpcResponse);
            }
        }

        return rpcMessage;

    }

    private void checkMagicNumber(ByteBuf frame) {

        int length = RpcConstant.MAGIC_NUMBER.length;
        byte[] bytes = new byte[length];
        frame.readBytes(bytes);

        for (int i = 0; i < bytes.length; i++) {
            if (bytes[i] != RpcConstant.MAGIC_NUMBER[i]) {
                throw new IllegalArgumentException("unKnow magic number:  " + Arrays.toString(bytes));
            }
        }
    }

    private void checkVersion(ByteBuf byteBuf) {
        byte version = byteBuf.readByte();
        if (version != RpcConstant.VERSION) {
            throw new RuntimeException("version is not compatible  " + version);
        }
    }
}
